W4. Операторы, выражения, структуры и объединения в C

Автор

Eugene Zouev, Munir Makhmutov

Дата публикации

23 сентября 2025 г.

Quiz | Flashcards

1. Краткое содержание

1.1 Операторы (statements) в C
1.1.1 Что такое statement?

В C statement (оператор) — это законченная инструкция, которая указывает компьютеру выполнить действие. Каждый оператор в C заканчивается точкой с запятой (;). В общем случае операторы не «возвращают значение» как выражение: их роль — управлять потоком выполнения или вызвать side effect (побочный эффект), например изменить значение переменной.

1.1.2 Классы операторов

Операторы C удобно группировать так:

  • Selection statements (операторы выбора): выбирают ветвь выполнения по условию.
    • if-else: выполняет блок при истинном условии и опциональный else при ложном.
    • switch: вычисляет целочисленное выражение и переходит к case с совпадающим значением. Обычно нужен break, иначе выполнение «проваливается» в следующий case.
  • Iteration statements (операторы цикла): повторяют блок, пока выполняется условие.
    • while: проверяет условие до каждой итерации тела.
    • do-while: выполняет тело как минимум один раз, затем проверяет условие после итерации.
    • for: компактно объединяет инициализацию, проверку и шаг после итерации.
  • Jump statements (операторы перехода): безусловно передают управление.
    • break: выходит из ближайшего цикла или switch.
    • continue: пропускает остаток текущей итерации цикла.
    • return: завершает текущую функцию и опционально возвращает значение.
    • goto: передаёт управление на метку в той же функции; обычно не рекомендуется из‑за читаемости и отладки.
  • Compound statements (составные операторы, блоки): ноль или несколько операторов в {}. Блок можно использовать там, где ожидается один оператор. Объявление внутри блока — тоже оператор.
  • Особые случаи:
    • Expression statement (оператор-выражение): выражение и ;. Значение выражения отбрасывается; важен побочный эффект (присваивание a = b + c;, вызов printf("hello");).
    • Declaration statement (оператор-объявление): вводит переменную (например int x = 10;). В современном C объявления допускаются везде, где допускается оператор, не только в начале блока.
1.2 Выражения (expressions) в C
1.2.1 Что такое expression?

Expression (выражение) — это «формула»: сочетание переменных, констант, операторов и вызовов функций, которое при вычислении даёт одно значение. Почти каждое выражение в C имеет значение.

1.2.2 Строительные блоки выражений

Выражения строятся из элементов по уровню сложности:

  1. Primary expressions (первичные выражения): базовые элементы.
    • Identifier: имя переменной или функции.
    • Literal: константа (123, 0.01E-2, "string").
    • Выражение в скобках: (a + b).
  2. Postfix expressions (постфиксные): над первичными.
    • Индекс массива: arr[i+j].
    • Вызов функции: func(*p, 777).
    • Доступ к полю: s.m (для struct/union) или ptr->m (указатель на struct/union).
    • Постфиксные ++/--: x++, x--.
  3. Unary expressions (унарные): один операнд.
    • Префиксные ++/--: ++x, --x.
    • Address-of (&) и indirection (*): &x, *p.
    • Унарный +/-: +x, -x.
    • Логическое НЕ (!) и побитовое НЕ (~).
    • sizeof: размер в байтах типа или объекта.
  4. Binary expressions (бинарные): два операнда (a + b, c * d).
  5. Ternary expression (тернарное выражение): условный оператор ? : (condition ? value_if_true : value_if_false).
1.2.3 Precedence и associativity
  • Precedence (приоритет): в сложном выражении задаёт порядок группировки операторов. Например, * и / выше, чем + и -, поэтому a + b * c — это a + (b * c).
  • Associativity (ассоциативность): для операторов одного приоритета задаёт порядок слева/справа. Большинство бинарных — слева направо (x - y + z как (x - y) + z). Унарные операторы и присваивание — справа налево (x = y = 5 как x = (y = 5)).
1.2.4 Side effects

Side effect (побочный эффект) — любое изменение состояния программы: запись в переменную, ввод-вывод и т.п. Выражения вроде x++ важны именно побочным эффектом: значением x++ является старое значение x, а побочный эффект — увеличение x.

1.3 Рекурсивные функции

Recursive function (рекурсивная функция) — функция, которая вызывает сама себя. Это естественно для задач, которые распадаются на похожие подзадачи меньшего размера.

1.3.1 Два обязательных компонента

Чтобы не зациклиться, у рекурсии должны быть:

  • Base case (базовый случай): условие, при котором рекурсии нет — сразу возвращается ответ.
  • Recursive step (рекурсивный шаг): вызов себя с аргументом, который приближает к базовому случаю.
1.3.2 Call stack при рекурсии

При каждом вызове функции на call stack (стек вызовов) кладётся кадр с её локальными переменными. При рекурсии на каждый самовызов добавляется новый кадр: стек растёт, пока не достигнут base case. Затем, когда каждый вызов возвращает результат своему вызывающему кадру, соответствующий кадр снимается (pop) — стек «разворачивается».

1.4 Структуры (struct)

Structure (структура, struct) — пользовательский тип, объединяющий связанные поля разных типов в одну логическую сущность.

1.4.1 Объявление и память

У каждого поля struct — своя область памяти. Полный размер — сумма размеров полей плюс memory padding (выравнивающие байты).

  • Memory padding: компилятор вставляет «пустые» байты между полями для выравнивания на адреса, удобные для железа (например int на границу 4 байт). Это ускоряет доступ, но увеличивает размер.
  • Packed structures: нестандартно (__attribute__((packed))) убирает padding; экономит память, но может ухудшить производительность или быть небезопасным на некоторых архитектурах.
1.4.2 Доступ к полям
  • Dot operator (.): у значения структуры, например student.id.
  • Arrow operator (->): у указателя на структуру, например studentPtr->id; эквивалентно (*studentPtr).id.
1.5 Объединения (union)

Union (объединение, union) — тип, где все поля разделяют одну и ту же область памяти.

1.5.1 Общая память

Под union выделяется столько памяти, сколько нужно самому большому полю. Запись в одно поле может перетирать байты других, потому что это одни и те же байты. Это полезно для экономии памяти или type punning (интерпретации одних и тех же байт по-разному). Осмысленно в каждый момент времени использовать «активное» поле одно.


2. Определения

  • Statement: законченная инструкция в C, заканчивается ;.
  • Expression: сочетание значений, переменных, операторов и функций, дающее одно значение при вычислении.
  • Expression statement: выражение с ;, выполняется ради побочных эффектов.
  • Declaration statement: объявляет (и опционально инициализирует) новую переменную.
  • Operator precedence: правила группировки операторов в выражении.
  • Side effect: изменение состояния (переменная, I/O и т.д.).
  • Recursion: приём, когда функция вызывает саму себя.
  • Base case: условие остановки рекурсии.
  • Structure (struct): тип с полями разных типов; поля в отдельных местах памяти.
  • Union (union): тип, где поля разделяют одну область памяти.
  • Memory padding: служебные байты внутри struct для выравнивания.
  • Typedef: ключевое слово для псевдонима типа, часто для читаемости struct/union.
  • Call stack: структура данных, хранящая информацию об активных подпрограммах (цепочке вызовов) программы.

3. Примеры

3.1. Разбор вывода: static и рекурсия (Лаба 4, Задание 1)

Каков ожидаемый вывод этой программы?

#include <stdio.h>

void func() {
    static int x = 5;
    int y = 5;
    while (y < 10 && x < 10) {
        printf("x = %d, y = %d\n", x, y);
        x++;
        y++;
        func();
    }
}

int main() {
    func();
}
Нажмите, чтобы увидеть решение

Проследим выполнение по шагам.

  1. main вызывает func().
    • func (вызов 1):
      • static int x = 5; выполняется один раз за всю программу: x создан и равен 5.
      • int y = 5; локальная y для этого вызова равна 5.
      • цикл while: условие (y < 10 && x < 10)(5 < 10 && 5 < 10), истина.
      • printf: x = 5, y = 5
      • x становится 6 (статический x теперь 6).
      • y становится 6.
      • рекурсивный func().
  2. func (вызов 2):
    • строка static int x = 5; пропускается, x остаётся 6.
    • новая локальная y = 5.
    • while: (5 < 10 && 6 < 10) — истина.
    • printf: x = 6, y = 5
    • x → 7, y → 6, рекурсия.
  3. func (вызов 3): x = 7, новая y = 5 → печать x = 7, y = 5x = 8, рекурсия.
  4. func (вызов 4): x = 8 → x = 8, y = 5x = 9, рекурсия.
  5. func (вызов 5): x = 9 → x = 9, y = 5x = 10, рекурсия.
  6. func (вызов 6): x = 10, y = 5 → (5 < 10 && 10 < 10) ложь, цикл не входит, возврат.
  7. Возврат в вызов 5: у его локальной y было 6, статический x = 10 → (6 < 10 && 10 < 10) ложь, цикл завершается, возврат.
  8. Аналогично «сворачивается» остальная рекурсия: условие x < 10 дальше нигде не выполняется, дополнительной печати нет.

Итоговый вывод:

x = 5, y = 5
x = 6, y = 5
x = 7, y = 5
x = 8, y = 5
x = 9, y = 5
3.2. Структуры студента и даты экзамена (Лаба 4, Задание 2)

Напишите программу с двумя структурами: student и exam_day. Первая содержит name, surname, group number и вложенную вторую структуру. Вторая — day, year и month экзамена. Поле month должно быть буквенным (например, May), не числом. Программа запрашивает все поля и печатает их.

Нажмите, чтобы увидеть решение
#include <stdio.h>
#include <string.h>

// Define the structure for the exam date.
struct exam_day {
    int day;
    char month[20]; // Character array to store the month's name
    int year;
};

// Define the structure for the student.
// This structure contains another structure as one of its members.
struct student {
    char name[50];
    char surname[50];
    int group_number;
    struct exam_day exam_date; // Nested structure
};

int main() {
    // Declare a variable of type 'student'.
    struct student s1;

    // --- Get User Input ---
    printf("Enter student's first name: ");
    scanf("%s", s1.name);

    printf("Enter student's surname: ");
    scanf("%s", s1.surname);

    printf("Enter student's group number: ");
    scanf("%d", &s1.group_number);

    printf("Enter exam day (e.g., 22): ");
    scanf("%d", &s1.exam_date.day);

    printf("Enter exam month (e.g., October): ");
    scanf("%s", s1.exam_date.month);

    printf("Enter exam year (e.g., 2025): ");
    scanf("%d", &s1.exam_date.year);

    // --- Print the Stored Information ---
    printf("\n--- Student Information ---\n");
    printf("Name: %s\n", s1.name);
    printf("Surname: %s\n", s1.surname);
    printf("Group: %d\n", s1.group_number);
    printf("Exam Date: %d %s %d\n", s1.exam_date.day, s1.exam_date.month, s1.exam_date.year);

    return 0;
}
3.3. «Шифрование» целого через union (Лаба 4, Задание 3)

С помощью union напишите программу: прочитать unsigned long long из консоли, затем «зашифровать», меняя местами каждый нечётный байт с соседним чётным, начиная со старшего байта. Должна быть функция encryption(...); напечатайте исходное, зашифрованное и расшифрованное значения.

Нажмите, чтобы увидеть решение
#include <stdio.h>

// A union allows storing different data types in the same memory location.
// Here, we can access the same 8 bytes of memory as either a single
// unsigned long long or as an array of 8 individual bytes (unsigned chars).
typedef union {
    unsigned long long ull_value;
    unsigned char bytes[8];
} ull_converter;

// Function to perform the byte-swapping encryption/decryption.
// The same logic works for both encryption and decryption.
void encryption(ull_converter *data) {
    // An unsigned long long is 8 bytes. We swap pairs of bytes:
    // bytes[0] with bytes[1]
    // bytes[2] with bytes[3]
    // bytes[4] with bytes[5]
    // bytes[6] with bytes[7]
    // The loop iterates 4 times for the 4 pairs.
    for (int i = 0; i < 8; i += 2) {
        // Use a temporary variable to swap the byte pair.
        unsigned char temp = data->bytes[i];
        data->bytes[i] = data->bytes[i+1];
        data->bytes[i+1] = temp;
    }
}

int main() {
    // Create a union variable.
    ull_converter data;

    // Prompt user for input.
    printf("Enter an unsigned long long integer: ");
    // Read the value from the user. %llu is the format specifier for unsigned long long.
    scanf("%llu", &data.ull_value);

    // Print the original value.
    printf("Original message: %llu\n", data.ull_value);

    // Call the encryption function. We pass the address of the union.
    encryption(&data);

    // The bytes inside the union have been swapped. Reading the ull_value now gives the encrypted number.
    printf("Encrypted message: %llu\n", data.ull_value);

    // Call the function again. Swapping the swapped bytes returns them to their original positions.
    encryption(&data);

    // Print the decrypted message, which should match the original.
    printf("Decrypted message: %llu\n", data.ull_value);

    return 0;
}